package com.autonavi.yunda.yunji.common.utils.geo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

/**
 * GeoBlockMgr
 *
 * @author mengpeng.mp
 * @date 2018/01/14
 */
@Service
public class GeoBlockMgr {
    public static int GEO_BLOCK_ID_LEN_500  = 13;
    public static int GEO_BLOCK_ID_LEN_1000 = 11;

    public static double CHINA_LAT_MIN = 17.0;
    public static double CHINA_LON_MIN = 73.0;

    private static final Map<Integer, Integer> X1_MAP = CreateX1Map();
    private static Map<Integer, Integer> CreateX1Map()
    {
        Map<Integer, Integer> ret = new HashMap<Integer, Integer>();
        ret.put(10, 10);
        ret.put(50, 5);
        ret.put(100, 1);
        ret.put(500, 5);
        ret.put(1000, 1);
        return ret;
    }

    private static final Map<Integer, Integer> X2_MAP = CreateX2Map();
    private static Map<Integer, Integer> CreateX2Map()
    {
        Map<Integer, Integer> ret = new HashMap<Integer, Integer>();
        ret.put(10, 1);
        ret.put(50, 10);
        ret.put(100, 100);
        ret.put(500, 100);
        ret.put(1000, 1000);
        return ret;
    }

    private static final Map<Integer, Integer> IDLEN_MAP = CreateIDLENMap();
    private static Map<Integer, Integer> CreateIDLENMap()
    {
        Map<Integer, Integer> ret = new HashMap<Integer, Integer>();
        ret.put(10, 7);
        ret.put(50, 6);
        ret.put(100, 5);
        ret.put(500, 4);
        ret.put(1000, 3);
        return ret;
    }

    private static final Map<Integer, Pair<Integer, Integer>> UNIT_MAP = CreateUNITMap();
    private static Map<Integer, Pair<Integer, Integer>> CreateUNITMap()
    {
        Map<Integer, Pair<Integer, Integer>> ret = new HashMap<>();
        ret.put(3, new Pair<Integer, Integer>(1000, 1));
        ret.put(4, new Pair<Integer, Integer>(100, 5));
        ret.put(5, new Pair<Integer, Integer>(100, 1));
        ret.put(6, new Pair<Integer, Integer>(10, 5));
        ret.put(7, new Pair<Integer, Integer>(1, 10));
        return ret;
    }

    /**
     * Calculate fixed scale blocks
     * @param points: list of wkt points
     * @param scale: block scale
     * @return block_ids: list of block ids
     */
    public static ArrayList<String> CalcFixedBlocks(ArrayList<GeoLatLon> points, int scale) {
        ArrayList<String> block_ids = new ArrayList<>();
        for (GeoBlock block: CalcBlocksWithScale(points, scale, true)) {
            block_ids.add(block.block_id);
        }
        return block_ids;
    }

    /**
     * 直接返回set的CalcFixedBlocks
     *
     * @param points
     * @param scale
     * @return
     */
    public static List<GeoBlock> CalcFixedBlocks2(ArrayList<GeoLatLon> points, int scale) {
        return CalcBlocksWithScale(points, scale, true);
    }


    /**
     * divide big block id to smaller block ids
     * @param blockID: big block id
     * @return subMatrix: small block ids matrix, left to right, top to bottom
     */
    public static ArrayList<ArrayList<String>> divideBlock(String blockID) {
        String[] pair = blockID.split("_");
        String geo_x = pair[0];
        String geo_y = pair[1];
        String tufu_x = geo_x.substring(0, 2);
        String tufu_y = geo_y.substring(0, 2);
        int loc_x = Integer.parseInt(geo_x.substring(2));
        int loc_y = Integer.parseInt(geo_y.substring(2));
        ArrayList<ArrayList<String>> result = new ArrayList<>();
        if (blockID.length() == GEO_BLOCK_ID_LEN_1000) {
            int size = 2;
            int big_scale = 10;
            for (int j = size - 1; j >= 0; j--) {
                ArrayList<String> line = new ArrayList<>();
                String y = tufu_y + String.format("%04d", (loc_y * big_scale + j * 5));
                line.add(y);
                for (int i = 0; i < size; i++) {
                    String x = tufu_x + String.format("%04d", (loc_x * big_scale + i * 5));
                    line.add(x);
                }
                result.add(line);
            }
        } else if (blockID.length() == GEO_BLOCK_ID_LEN_500) {
            int size = 5;
            int big_scale = 1;
            for (int j = size - 1; j >= 0; j--) {
                ArrayList<String> line = new ArrayList<>();
                String y = tufu_y + String.format("%05d", (loc_y * big_scale + j));
                line.add(y);
                for (int i = 0; i < size; i++) {
                    String x = tufu_x + String.format("%05d", (loc_x * big_scale + i));
                    line.add(x);
                }
                result.add(line);
            }
        }
        return result;
    }

    private static List<GeoBlock> CalcBlocksWithScale(ArrayList<GeoLatLon> points, int scale, boolean expand) {
        List<GeoBlock> ret = new ArrayList<>();

        if (scale != 10 && scale != 50 && scale != 100 && scale != 500 && scale != 1000)
        {
            scale = 1000;
        }

        List<Double> env = GetEnvelop(points);
        double env_lon_min = env.get(0);
        double env_lon_max = env.get(1);
        double env_lat_min = env.get(2);
        double env_lat_max = env.get(3);

        double start_lon = CHINA_LON_MIN;
        double start_lat = CHINA_LAT_MIN;
        double earthWDZC=40008.08*1000;

        //Here comes lat loop
        double lat_unit=0.000009000900090009001;
        int lat_cnt = (int)((env_lat_max - env_lat_min) / (lat_unit * scale)) + 2;

        double cur_lat = env_lat_min;
        for (int i=0; i<lat_cnt; i++) {
            int block_index_lat = (int)(Math.floor((cur_lat - start_lat)/2));
            Pair<Integer, Double> lat_idx = CalcIdx(cur_lat, start_lat, block_index_lat, lat_unit, scale, X1_MAP.get(scale), X2_MAP.get(scale));
            int cur_lat_index = lat_idx.x1;
            double cur_lat_min = lat_idx.x2;

            //Process irregular blocks
            double cur_lat_jump = lat_unit * scale;
            double cur_lat_max = cur_lat_min + cur_lat_jump; // lat_unit * scale;
            if (cur_lat_max > (block_index_lat+1) * 2 + start_lat) {
                cur_lat_max = (block_index_lat+1) * 2 + start_lat;
            } else {
                if (cur_lat_max + cur_lat_jump > (block_index_lat+1) * 2 + start_lat) {
                    cur_lat_jump = (block_index_lat+1) * 2 + start_lat - cur_lat_max;
                }
            }
            cur_lat = cur_lat_max + cur_lat_jump / 2;

            double earth_len = earthWDZC * Math.cos((2*Math.PI/360) * (block_index_lat*2 + start_lat + 1));
            double lon_unit = 360.0 / earth_len;
            int lon_cnt = (int)((env_lon_max - env_lon_min)/(lon_unit*scale)) + 2;

            double cur_lon = env_lon_min;
            for (int j=0; j<lon_cnt; j++) {
                int block_index_lon = (int)(Math.floor((cur_lon - start_lon)/2));
                Pair<Integer, Double> lon_idx = CalcIdx(cur_lon, start_lon, block_index_lon, lon_unit, scale, X1_MAP.get(scale), X2_MAP.get(scale));
                int cur_lon_index = lon_idx.x1;
                double cur_lon_min = lon_idx.x2;

                double cur_lon_jump = lon_unit * scale;
                double cur_lon_max = cur_lon_min + cur_lon_jump; // lon_unit*scale;
                if (cur_lon_max > (block_index_lon+1) * 2 + start_lon) {
                    cur_lon_max = (block_index_lon+1) * 2 + start_lon;
                } else {
                    if (cur_lon_max + cur_lon_jump > (block_index_lon+1) * 2 + start_lon) {
                        cur_lon_jump = (block_index_lon+1) * 2 + start_lon - cur_lon_max;
                    }
                }
                cur_lon = cur_lon_max + cur_lon_jump / 2;

                int id_len = IDLEN_MAP.get(scale);

                String cur_lat_id = String.format("%02d", block_index_lat) + String.format("%0"+String.valueOf(id_len)+"d", cur_lat_index);
                String cur_lon_id = String.format("%02d", block_index_lon) + String.format("%0"+String.valueOf(id_len)+"d", cur_lon_index);


                if (expand) {
                    double center_lat = (cur_lat_min + cur_lat_max) / 2;
                    double center_lon = (cur_lon_min + cur_lon_max) / 2;
                    if (IsPtInPoly(cur_lat_min, cur_lon_min, points) ||
                            IsPtInPoly(cur_lat_min, cur_lon_max, points) ||
                            IsPtInPoly(cur_lat_max, cur_lon_min, points) ||
                            IsPtInPoly(cur_lat_max, cur_lon_max, points) ||
                            IsPtInPoly(center_lat, center_lon, points) ||
                            IsPtInPoly(center_lat, cur_lon_min, points) ||
                            IsPtInPoly(cur_lat_min, center_lon, points) ||
                            IsPtInPoly(center_lat, cur_lon_max, points) ||
                            IsPtInPoly(cur_lat_max, center_lon, points)) {
                        ret.add(new GeoBlock(cur_lat_min, cur_lon_min,
                                        cur_lat_max, cur_lon_max,
                                        cur_lon_id + "_" + cur_lat_id)
                        );
                    }
                } else {
                    double center_lat = (cur_lat_min + cur_lat_max) / 2;
                    double center_lon = (cur_lon_min + cur_lon_max) / 2;
                    if (IsPtInPoly(center_lat, center_lon, points)) {
                        ret.add(new GeoBlock(cur_lat_min, cur_lon_min,
                                cur_lat_max, cur_lon_max,
                                cur_lon_id + "_" + cur_lat_id)
                        );
                    }
                }
            }
        }
        return ret;
    }

    /*
    * Never ask me what x1 and x2 means.
     */
    private static Pair<Integer, Double> CalcIdx(double cur_v, double start_v, int block_index, double unit, int scale, int x1, int x2) {
        Pair<Integer, Double> ret = new Pair<Integer, Double>();
        ret.x1 = (int)((cur_v - start_v - block_index*2) / (unit*scale)) * x1;
        ret.x2 = block_index*2 + start_v + ret.x1 * unit * x2;
        return ret;
    }

    private static ArrayList<Double> GetEnvelop(ArrayList<GeoLatLon> points) {

        ArrayList<Double> ret = new ArrayList<Double>();
        double lon_min = 180.0f;
        double lat_min = 90.0f;
        double lon_max = 0.0f;
        double lat_max = 0.0f;

        for (int i=0; i<points.size(); i++) {
            lon_min = Math.min(points.get(i).lon, lon_min);
            lon_max = Math.max(points.get(i).lon, lon_max);

            lat_min = Math.min(points.get(i).lat, lat_min);
            lat_max = Math.max(points.get(i).lat, lat_max);
        }

        ret.add(lon_min); ret.add(lon_max);
        ret.add(lat_min); ret.add(lat_max);
        return ret;
    }

    private static boolean IsPtInPoly(double aLat, double aLon, ArrayList<GeoLatLon> points) {

        if (points.size() < 3)
        {
            return false;
        }

        int cross_num = 0;
        for (int i=0; i<points.size(); i++) {
            double pLon1 = points.get(i).lon;
            double pLat1 = points.get(i).lat;

            double pLon2 = points.get((i+1)%points.size()).lon;
            double pLat2 = points.get((i+1)%points.size()).lat;
            if ((aLat >= pLat1 && aLat < pLat2) || (aLat>=pLat2 && aLat < pLat1)) {
                if (Math.abs(pLat1 - pLat2) > 0) {
                    double pLon = pLon1 - ((pLon1 - pLon2) * (pLat1 - aLat)) / (pLat1 - pLat2);
                    if (pLon < aLon) {
                        cross_num++;
                    }
                }
            }
        }
        return (cross_num%2) != 0;
    }

    private static double distance(GeoLatLon x, GeoLatLon y) {
        double lgt1 = x.lon;
        double lat1 = x.lat;
        double lgt2 = y.lon;
        double lat2 = y.lat;

        double dx = lgt1 - lgt2;
        double dy = lat1 - lat2;
        double b = (lat1 + lat2) / 2.0;
        double Lx = radian(dx) * 6367137.0 * Math.cos(radian(b));
        double Ly = 6367137.0 * radian(dy);
        return Math.sqrt(Lx * Lx + Ly * Ly);
    }

    private static double radian(double angle) {
        return (Math.PI * angle) / 180.0;
    }

    public static class Pair<TYPE1, TYPE2> {
        public TYPE1 x1;
        public TYPE2 x2;

        private Pair(){}

        private Pair(TYPE1 x1, TYPE2 x2) {
            this.x1 = x1;
            this.x2 = x2;
        }
    }

    /**
     * convert xy to block id
     * @param x: x, longitude
     * @param y: y, latitude
     * @param scale: block scale
     * @return block_id: block id
     */
    public static String xy2BlockId(String x, String y, int scale) {
        double startlon = CHINA_LON_MIN;
        double startlat = CHINA_LAT_MIN;
        double xVal = Double.parseDouble(x);
        double yVal = Double.parseDouble(y);
        int indexX = (int) Math.floor((xVal - startlon) / 2);
        int indexY = (int) Math.floor((yVal - startlat) / 2);

        double earthWDZC = 40008.08*1000;
        double earchLength = earthWDZC * Math.cos(Math.PI * (indexY * 2 + startlat + 1) / 180);

        /**
         * Attention here, this unit is different from any other function
         */
        double wd1Unit = 0.00009000900090009001;
        double jd1Unit = 3600 / earchLength;
        double wdQk = yVal - startlat - indexY * 2;
        double jdQk = xVal - startlon - indexX * 2;

        int wdIndex = (int) ((wdQk / wd1Unit) / (scale / 10)) * X1_MAP.get(scale);
        int jdIndex = (int) ((jdQk / jd1Unit) / (scale / 10)) * X1_MAP.get(scale);
        String wdInIndex = String.format("%0" + String.valueOf(IDLEN_MAP.get(scale)) +"d", wdIndex);
        String jdInIndex = String.format("%0" + String.valueOf(IDLEN_MAP.get(scale)) +"d", jdIndex);

        indexX += indexX < -90 ? 180 : 0;
        String fIndexX = String.format("%0" + (indexX >= 0 ? "2" : "3") + "d", indexX);
        String fIndexY = String.format("%0" + (indexY >= 0 ? "2" : "3") + "d", indexY);

        return fIndexX + jdInIndex + "_" + fIndexY + wdInIndex;
    }


    /**
     * convert block id to xy
     * @param blockId: block id
     * @return xy: x, y (longitude, latitude)
     */
    public static Pair<String, String> blockId2Xy(String blockId) {
        GeoBlock geoBlock = blockId2GeoBlock(blockId);
        if (geoBlock == null) {
            return null;
        }
        Double x1 = geoBlock.getCur_lon_min();
        Double y1 = geoBlock.getCur_lat_min();
        Double x2 = geoBlock.getCur_lon_max();
        Double y2 = geoBlock.getCur_lat_max();

        String x = Double.toString((x1 + x2) / 2);
        String y = Double.toString((y1 + y2) / 2);

        return new Pair<>(x, y);
    }

    /**
     * convert block id to geoblock object with vertices coordinate
     * @param blockId
     * @return geoBlock
     */
    public static GeoBlock blockId2GeoBlock(String blockId) {
        String[] idParts = blockId.split("_");
        int length2 = 2;
        if (idParts.length != length2) {
            return null;
        }

        double startlon = CHINA_LON_MIN;
        double startlat = CHINA_LAT_MIN;
        double earthWDZC = 40008.08*1000;
        double wd1Unit = 0.000009000900090009001;

        int tufuX = Integer.parseInt(idParts[0].substring(0, 2));
        int tufuY = Integer.parseInt(idParts[1].substring(0, 2));
        String locXStr = idParts[0].substring(2);

        double middleWd = startlat + tufuY * 2 + 1;
        double jd1Unit = 360 / (earthWDZC * Math.cos(Math.PI * middleWd / 180));

        Pair<Integer, Integer> unitNext = UNIT_MAP.get(locXStr.length());
        int unit = unitNext.x1;
        int next = unitNext.x2;
        int locX = Integer.parseInt(locXStr);
        int locY = Integer.parseInt(idParts[1].substring(2));

        double x1 = tufuX * 2 + startlon + locX * jd1Unit * unit;
        double y1 = tufuY * 2 + startlat + locY * wd1Unit * unit;
        double x2 = tufuX * 2 + startlon + (locX * 1 + next * 1) * jd1Unit * unit;
        double y2 = tufuY * 2 + startlat + (locY * 1 + next * 1) * wd1Unit * unit;

        return new GeoBlock(y1, x1, y2, x2, blockId);
    }



}
